Supply the motion verbs OPC 40010 leaves undefined - #47
Conversation
OPC 40010-1 describes robot topology in detail and defines no motion verbs at all: its whole actuation surface is Start, Stop and loading a named program. A client can discover everything about a robot's construction and cannot ask it to move anywhere. This adds OPC UA - Robot Intent, which supplies the verbs and nothing else, so the two compose rather than compete. The lifecycle is the harder half. A motion takes seconds and a pick takes a minute, while OPC 10000-4 clause 5.12.2 discards a method result when the Session ends "independent of the task actually performed at the Server" - so a synchronous motion method loses the outcome of work that has already physically happened. Submission therefore returns a Part 10 program instance and the client watches it, which is the resolution OPC 10000-10 clause 4.1 already reaches for exactly this case. Verbs are a DataType hierarchy rather than one Method each, so a single submission and a mission step are the same shape and extension is subtyping. Queueing is PLCopen MC_BufferMode and concurrency is VDA 5050 blockingType, both adopted unchanged because both are already implemented everywhere and no OPC UA specification defines either. Missions carry an immutable committed base and a revisable horizon. Orientation is a unit quaternion: OPC UA defines no quaternion type, and the A/B/C fields of ThreeDOrientation carry no convention of their own. Annex C carries the bidirectional conversion, including the clamp that stops a pole orientation becoming a domain error. Pick and Place reference Location nodes rather than naming a station in a string, which would be a second definition of a fact the address space already holds. Clause 10 declares the interface non-safety-rated, restricts submission to Automatic and Automatic External, and records that a stop request selects no IEC 60204-1 stop category. Clause 8 says plainly that command authority arbitrates between clients and is not the single point of control ISO 10218-2 requires. The NodeSet is standalone on base OPC UA, following the Vision precedent; OPC 40010-1 interop is an optional profile carried by a ReferenceType. The validator re-derives from the committed artifacts rather than asking the generator what it emitted, and cross-checks the specification against the model in both directions. It was mutation-tested against thirteen defects, including one introduced in the generator and fully regenerated so that the NodeSet, the CSV and Annex A all agree with each other - which only the specification invariants can catch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa
A DataTypeEncoding browses as "Default Binary" in namespace 0. The BrowseName is standard, not model-defined, and every published companion NodeSet writes it that way. Both generators were emitting 1:Default Binary. This is not cosmetic. Tooling that resolves encodings by BrowseName cannot find them: the UA-.NETStandard model source generator fails outright on such a NodeSet, and the failure surfaces as a NullReferenceException with no indication of which node caused it - it took a binary search over the node list to find. Both models are regenerated. No NodeId moves, because only the BrowseName and the added SymbolicName change: Vision is nine lines, Robot Intent eighteen. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa
47a8b3d to
f5789cb
Compare
Five things the specification did not do. Four are now defined; the fifth turns out not to be a scoping choice at all. Real-time was an exclusion and is now a division of labour. Trajectory execution is expressible over request/response - the whole path is handed over once and the robot's own motion kernel runs it, which is what FollowJointTrajectory and the PLCopen buffered path blocks already do - so TrajectoryIntentDataType, CartesianPathIntentDataType and ForceIntentDataType are defined here. Streaming control is not expressible, so the model brokers a channel instead: RealTimeChannelType describes and leases RTDE, EGM, FRI, RSI, MotoROS2 or OPC UA FX and the samples never traverse OPC UA. That is the same shape as the Vision model brokering a media endpoint rather than carrying pixels. Safety cannot be brought into scope as a rating, and the reason is structural rather than editorial. OPC 10000-15 carries cyclic safety data from a provider to a consumer; the consumer's request holds an identifier, a monitoring number and one octet of explicitly non-safety flags, so a caller has no channel through which to supply safety-rated arguments. Every safety fieldbus expresses a safety command as a continuously asserted cyclic signal, because the integrity argument rests on the fail-safe state that follows when assertion stops - and a Method call has no defined behaviour when it stops being called. So safety is in scope as awareness plus a duty: SafetyStateType reports what the safety system enforces, clause 10.4 requires a Server to refuse work that would exceed it, and the same clause says plainly that neither of those makes anything safe. A Server may observe the safety system and refuse on what it sees; it may never instruct it. Robot construction was excluded on the grounds that OPC 40010 covers it. It does not cover this part: OPC 40010-1 describes topology and axes and defines no kinematic chain an IK solver could use, and no tool centre point at all. RobotDescriptionType is therefore additive, and Annex B fixes which side decides where both are present. Process models are defined for all six applications, with fastening left deliberately thin: OPC 40450 and OPC 40451 already own joining and tightening, so FastenIntentDataType references a joint there rather than restating torque strategies - the rule that already made Pick take a Location node instead of a station string. Mission branching follows IEC 61131-3 rather than a behaviour tree. Steps and transitions with alternative and parallel divergence are the notation the controller audience knows and have an IEC serialization; a behaviour tree needs a tick runtime controller vendors do not provide. Transition conditions reuse the base UA ContentFilter, so nobody has to write a parser for an invented expression language, and an empty transition array leaves a mission the flat sequence it was. 83 nodes added, 0 removed, 0 renumbered. The validator gained invariants for the process hierarchy, the ContentFilter reuse and the mandatory safety members, and the mutation suite grew from 13 defects to 17 - all caught, with the tree restored byte-exactly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa
|
Stacked below #49. That PR branches from this one and restructures what this specification interoperates with, so this one should land first. Relevant to reviewing this PR: #49 adds Annex E and the |
The prose said TrajectoryIntentDataType carries PathTolerance, GoalTolerance and GoalTimeTolerance "all MotionToleranceDataType". GoalTimeTolerance is a Duration. An implementer decoding the structure from the prose gets the wire format wrong at that field, and the structure is not self-describing there. Lateness is one number, not a pose deviation. The 6.3 state diagram had Retriable --> Queued and gave Retriable no exit to terminal, so execution resumed on the same instance. Clause 3 defines Retriable as a state "from which execution does not resume", 6.6 says Retry creates a NEW operation with the original left terminal, and 6.7 says a terminal Result shall not change thereafter. The model cannot distinguish the two readings - Retriable to Queued is Halted to Ready, which Part 10 permits - so a conformance test built from the model alone rejects neither, and two implementations get incompatible operation lifetimes. FinalResultData and ProgramDiagnostic are Optional in ProgramStateMachineType and were not promoted. So 6.7's "shall also be reachable under FinalResultData" rested on a member a conformant Server may omit, and 1.2's headline "auditable commanding" claim - which IS ProgramDiagnostic and nothing else - was false against a legal implementation. Both are now Mandatory on IntentOperationType, which is the repository's stated remedy: a conformance unit that depends on an inherited member promotes that member. Written beside their type the promotions renumbered 39 members; they are allocated at the end of the id space instead. 0 moved. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa
Review response — 3 High findings fixed
The state diagram let execution resume from a state the document defines as terminal. §6.3 had Two normative claims rested on Optional Part 10 members that were never promoted. §6.7's "shall also be reachable under Writing the promotions beside their type renumbered 39 members; they are allocated at the end of the id space instead. 0 moved. 9/9 checks green. Not fixedSeven Medium findings are left for a follow-up rather than folded in here, since none is a correctness bug in the model: three |
Implementing this specification in the OPC UA .NET Standard stack surfaced six defects. Each is a claim the document already made that the model did not let a conformant Server keep. Clause 6.2 orders six refusals and clause 5.8 calls the failure set small and diagnosable on purpose, but SubmitIntent returned only IntentId and Operation. A client could see that something had been refused and never which of the six it was, so the ordering was unobservable and the failure taxonomy unreachable. SubmitIntent, SubmitMission and Retry now return Accepted, Failure and Message, clause 6.2 states that a Server returns Good and reports the refusal there rather than substituting a Bad StatusCode, and OpenRealTimeChannel gains a Message because clause 6.9 hid four grounds for refusal behind one boolean. IntentOperationType declared ProgramDiagnostic as a Property of PropertyType reached by HasProperty. OPC 10000-10 declares it as a Variable of ProgramDiagnostic2Type reached by HasComponent, so the declaration added a second member beside the inherited one instead of promoting it - which is why a Part 10 client would not have found it where Part 10 says it is, and why the first stack to generate the model deleted it. A promotion changes the ModellingRule and nothing else; clause 6.1 now says so. WaitIntentDataType.Signal read "an OutputSignal or other node", which clause 11.3 cannot check, and an unvalidated NodeId is the surface clause 11.3 exists to close. It is now bounded to an OutputSignal under the controller or a Boolean Variable under it, and clause 11.3 tabulates the expected type of every NodeId-valued member rather than leaving "the expected type" to the reader. Ready, ActiveIntent, ActiveMission and ControlOwner were in the model and in no clause; clause 5.7.0 gives them normative meaning. Clause 6.9 now bounds RequestedLease, which the lease rules never limited. The NodeSet, the CSV and Annex A are regenerated; no previously assigned NodeId moves.
Implementing the specification a second time surfaced three more places where a conformant Server could tell a client something untrue. Clause 9 already had three rules keeping the capability declaration honest, but none of them said that a declared capability must come with the Methods that make it usable. SubmitMission, UpdateMission, CancelMission and both real-time channel Methods are Optional on IntentControllerType, so a Server could report MissionsSupported true and omit SubmitMission entirely - which is exactly what the first implementation did, and the contradiction only surfaced when a client called something that was not there. A fourth rule and a table now fix which Methods each declaration implies, and like the other three it is checkable by browsing a running Server. Clause 6.5 gave StopMode the PossibleStopModes vocabulary of OPC 40010-1 and then said nothing about a Server that treats every value alike, so accepting the argument and discarding it looked conformant. A Server must now either honour it or treat every value as its single stop behaviour, and should say which - a client that asks for OnPath and silently receives a QuickStop has been told something untrue about how the cell came to rest. Clause 6.4 left the stop mode of a superseded intent undefined, because an Aborting submission names none. The Server chooses, should choose the most urgent stop the cell tolerates since the successor is about to command motion of its own, and should document which. Prose only; the model is unchanged and regenerates identically.
Clause 12 defined conformance in terms of facets and then gave a Server nowhere to say which ones it had. A client that wanted to know whether a controller supported blending had to re-derive the whole of Table 12.2 from the address space itself, which is what the reference implementation ended up doing. That is worse than verbose. Several rows of the table are behavioural -- "the four blending buffer modes accepted and honoured", "the refusal rules of 6.2", "the base immutability rules of 7.2" -- and no amount of browsing settles them. Two clients deriving conformance independently could therefore reach opposite conclusions about the same Server and both be reading the specification correctly. OPC 10000-5 already answers this with ServerCapabilitiesType. ServerProfileArray: the Server states its profiles rather than leaving them to be inferred. IntentCapabilitiesType now carries SupportedFacets on the same principle, Mandatory, naming the facets of Table 12.2 verbatim. Clause 12.2 now separates the two kinds of requirement it had been mixing. Structural requirements are settled by reading -- the Outputs folder, more than one ToolType, axes covering 0 to AxisCount - 1 -- and a Server shall not list a facet whose structural requirements are unmet. Behavioural requirements are the Server's attestation, and clause 9 says what that means: listing RI-Blending while treating the buffer modes as Buffered is the same false statement rule 3 already forbids, whatever BlendingSupported says. RI-Base requires SupportedFacets, since a conformance claim that cannot be read is not a claim. The member appends at i=6139; no existing NodeId moves.
Two defects, both found by implementing the draft and reviewing the result. The promotions of FinalResultData and ProgramDiagnostic were emitted with namespace-1 BrowseNames. OPC 10000-3 says a subtype overrides an inherited InstanceDeclaration by re-declaring a child with the same QualifiedName, and a QualifiedName includes the namespace index -- so 1:ProgramDiagnostic never overrode 0:ProgramDiagnostic, it declared a second member beside it and left the inherited Optional one in place. That is the same failure the TypeDefinition and reference type were already careful to avoid, reached by a different route, and it defeated the entire point of promoting: clause 6.7's SHALL and the clause 1.2 auditable-commanding claim still rested on members a conformant Server could omit. The generated client made it visible -- its accessor used `new` to hide the base Part 10 one. Both now carry namespace-0 BrowseNames, using the same mechanism already applied to DataTypeEncoding browse names for the same reason, and every other NodeSet in the reference stack promotes inherited state-machine members unprefixed. Clause 6.6 required Pause to suspend execution retaining position, but the model defines no channel through which a Server can tell an actuator to pause mid-motion. A Server whose controller offers no pause could therefore only publish Suspended and hope -- and Part 10 defines that state as position retained, so an operator reading the HMI sees the arm at rest while it finishes the move. The reference implementation did exactly that until a review caught it. The requirement stays, because stopping the robot is what Pause should mean. What is added is the honesty rule the rest of clause 9 already applies elsewhere: a Server that cannot suspend a running intent declares PauseSupported false, may still stop its queue, and leaves the executing operation Executing with Ready reflecting that nothing new is admitted. Suspended is never reported while the robot is still moving. That makes clause 9 four rules rather than three, alongside the BlendingSupported rule it is modelled on. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a248a589-9a20-4372-868e-5d347e57001b
…#49) * Give Vision a frame role for the flange Vision's frame vocabulary was World, Base, Tool, Camera, Object, Other. It had no value for the mechanical interface, so its own eye-in-hand picking example labelled the flange `Tool` and the frame tree contained no tool centre point at all. That is not a naming quibble. The hand-eye calibration in Annex F resolves camera to flange, and a pick has to reach the TCP. With one `Tool` role the example could not carry the offset between them: adding a second frame for the real TCP would have made `Tool` ambiguous inside one tree. Vision 5.8 describes this exact failure - "the pose is correct and the robot moves to the wrong place, because the two ends disagreed about what the numbers were relative to" - and the annex reproduced it. `MechanicalInterface` is added and the literals are renumbered to match Robot Intent, which had it right. Renumbering is free today and permanent after publication: enum values live in EnumStrings, so no NodeId moves - the CSV diff is 0 changed, 0 removed, 0 added for both models. The example descriptor gains a `GripperTcpFrame` so the picking example carries the flange-to-TCP offset explicitly rather than leaving a consumer to assume it, and says why the two frames are distinct. Annex F, both addenda and the overlays regenerate from it. Vision 5.12 gains the three pose rules it was missing beside Robot Intent's: right-handedness, quaternion normalisation, and the statement that this model defines no default frame. The last one matters most - the same empty `FrameId` means "not actionable" here and "default work frame" there, so a transcoder written by someone who read one specification would have produced a pose in the wrong frame. Ten bare "5.10" references cited DataTypes when the rules they name - corner datum, distortion ordering, frame precedence, the covariance sentinel - are all in 5.12. They passed CI because check_section_refs.py requires a literal section sign to see a reference at all. Both specifications now carry a mutual interop annex, which neither did: they never referenced each other despite being deployed on the same robot. Vision Annex I and Robot Intent Annex E fix which frame tree decides, how FrameId values correspond, that roles map by name and never by integer, how poses transcode across the covariance field, and that a grasp pose resolving only to the mechanical interface is refused. Annexes only - no NodeIds, no NodeSet dependency, and both models keep base OPC UA as their only RequiredModel. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa * Give the AI model its own specification Vision had grown a model nameplate, a dataset, a deployment and a learning job - 30 members and ten types - none of which mention a camera. A process soft sensor needs all of it and none of a lens, so leaving it there would have obliged every other domain either to declare a vision model as a RequiredModel or to define the whole thing again, at which point the provenance question stops having one answer. metaverse-specs/ai-deployment/ is domain-neutral by construction: its validator fails the build if a type name acquires a domain term, and the check is CamelCase word-boundary matched so Framework is not a false hit. DeploymentStateEnum replaces the VisionEndpointStateEnum the deployment had borrowed - same five literals, no dependency. The join is a facet precondition, not a RequiredModel. InferencePipelineType.Deployment was already a plain NodeId, so Vision's namespace index does not shift and a Server that publishes cameras and verdicts without ever naming a model is still conformant to VIS-Base. VIS-Inference-* and VIS-Learning require AI-Base, and say so. No Vision NodeId moves. The vacated blocks - 6107..6136, 6177..6190, 6193, 6195 - are reserved rather than reclaimed, because reusing them would renumber all 45 members declared after them. The CSV diff is 61 removed, 0 added, 0 moved. Both validators now cross-check specification against model in both directions. That found 16 real gaps in Vision: enum literals and structure fields the model declared and the prose had truncated with "and so on". Mutation-tested in both directions on both models. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa * Stop restating the learning loop in two places Clause 9.5 carried StartCollection, StopCollection, TriggerTraining and PromoteModel with their StatusCodes, and 9.6 carried the state model - all of which LearningJobType owns and OPC UA - AI Deployment and Learning now states. Two documents stating one transition table is two places for it to be wrong, and the wrong one is found by an implementer rather than by a validator. 9.5.1 keeps only the vision-specific half: the join between a correction submitted through VisionFeedbackType and the job that consumes it. The distinct PromoteModel authorization is deliberately stated in both documents, because it is the one rule where a reader of either alone would reach the wrong conclusion. The both-directions cross-check then found a real defect the old prose had hidden. Clause 9.2 was titled "The three purposes" and described three; VisionFeedbackPurposeEnum declares four. Trigger had never been documented - it only appeared to be, because a "| From | Trigger | To |" table header in the deleted state model satisfied the word match. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa * Let a Server actually use a model, not just describe one The model said what a model IS and never let anyone use one. EndpointUri was a bare String: no wire contract, no credential, no capabilities, no failure semantics. A Server holding one knew where to send bytes and nothing about what shape they should take or what to do when the far end stopped answering - so "consume a model hosted elsewhere", which is the whole point of the InferenceLocation property, was unimplementable from what this model told it. Three additions. Clause 7 defines one invocation surface whose signature does NOT change with InferenceLocation. That is not an aspiration: serving runtimes that run on a workstation and the hosted services they mirror already expose the same contract, differing only in addressing and auth. The payload stays opaque - it is domain vocabulary - and what gets fixed is the envelope, where ModelUsed reports the model that ACTUALLY answered. A fallback or a followed reference moves it between the call and the read, so the deployment's current model is the wrong answer to "what produced this result", and wrong in the direction that looks plausible. Clause 8 makes remote consumption real: dialect, auth kind, a credential REFERENCE that is never a secret, capabilities, reachability, and the question no inference API answers because it can assume its caller will wait - what the line does when the WAN drops. FallbackPolicy is Mandatory. So is DataJurisdiction and EgressPermitted, because encryption answers who can read data in flight and not where it went. Clause 9 makes the catalogue an xRegistry domain extension rather than a private invention. Two things then fall out of the base type instead of being designed: ResourceType IS a FileType, so a staged artefact streams through the inherited Open/Read/Close; and it already carries ExternalReference, so a federated entry whose bytes live elsewhere is expressible without pretending to hold them. The import job federates by default and stages when the target could not otherwise reach the source, and verifies the digest at staging - the one moment a substituted artefact would enter. Jobs now derive from the Part 10 ProgramStateMachineType through a shared AiJobType, as Robot Intent does, so the transition events and auditability are inherited rather than reinvented. Costs, stated plainly rather than buried: this model now requires OPC UA - xRegistry, which spends the "exactly one RequiredModel" claim and moves the own namespace from ns=1 to ns=2. Both validators hardcoded ns=1 while reading it - Vision's would have resolved nothing and passed. Both now derive the index from NamespaceUris. No NodeId moved. JobId and LastError moved up to AiJobType and their ids are reserved, not reclaimed: 0 removed in place, 141 added, 0 moved. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa * Enforce the new rules, and show what they add up to The invariants that make clauses 7 to 10 executable rather than aspirational, each mutation-tested: - The four Mandatory members clause 8 rests on. A rule resting on an Optional member is a rule a conformant Server can silently not satisfy. - Invoke returns ModelUsed, Usage and FinishReason. - Nothing in the address space is NAMED like credential material. That is how the clause 8.2 prohibition gets violated by accident, and the address space is browsable, subscribable and historisable, so a secret placed here is not merely readable - it is archived. - The catalogue types narrow their inherited placeholders. A domain extension that leaves them open adds metadata while restricting nothing. - AiJobType still derives from the Part 10 base. - NamespaceUris order, because it fixes every namespace index in the file. All seven mutations caught; artifacts restored byte-exactly. Annex C works one arrangement end to end: a federated cloud model and a staged edge model, the WAN dropping, and the fallback answering. The point of it is the sentence a table cannot make - the client called the same Method with the same arguments throughout and never learned that inference had moved, except by reading ModelUsed, which is exactly what an audit a month later needs and what a client logging the deployment would have got wrong. ApiDialectEnum.OpenAiCompatible became RestChatCompletions. The literal named a company. A standard's enumeration should describe the contract, not advertise whoever published it first - and the prose says what it is compatible with without needing the name. No vendor is named anywhere in the document. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa * Deepen the clauses that were already there Adding three clauses left the existing ones describing a smaller model than the one that now exists, and put 5.6 between 5.1 and 5.2 - the out-of-document-order defect this repository has been bitten by before and which no check catches. 5.6 moved to where it belongs. Every heading is now in order. 5.2 documents what a model carries beyond its artefact: the publisher that completes the identity triple, and Quantization, which is not a packaging detail - a quantized model is a different artefact that gives different answers, and treating it as a variant is how a model evaluated at full precision ends up deployed at reduced precision with nobody re-measuring it. 5.3 says why Mixed is not a hedge, and that a dataset whose class list disagrees in ORDER with its model is undetectable: every identifier resolves, every count is plausible, every label is off by one. 5.4 gains a table of everything that makes a deployment callable, and the reason State and Reachability are separate. Ready + Unreachable is a network fault; Faulted + Reachable points at credentials. Collapsing them loses exactly what a commissioning engineer needs. 6 gains the joins to the consuming half - a trained candidate is not privileged over an imported model and owes the same lineage - and says plainly that almost no Server implements the whole loop, which is why State is read rather than inferred from which Methods exist. 11 grew because the attack surface did. The chain has more links; two break while a Server looks correct. The resolver policy now governs seven URIs, and applying it to a staging import AFTER transfer is not a control. Credential material is unaddressable, argued from the fact that the address space is subscribable and historisable - a secret there is not exposed once, it is published and archived. FollowsRef is promotion's second door and needs the same authorization, or the audit trail shows every promotion having been authorized. And a fallback may not be more permissive on egress than what falls back to it, or a network fault quietly sends plant data where policy forbids. 11.4 now says what DigestVerified actually means: the artefact is the one this catalogue entry described. An attacker who can edit both artefact and entry defeats it, and the catalogue is the better target because many machines read it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa * Fix what the review found in the AI split Critical: the prose described a root object the model does not have. VisionRootType lost Models and LearningJobs when the AI cluster moved, but 4.2 still listed them normatively, 5.3 still said "five folders", and the 5.2 figure still drew them. A Server following 4.2 literally would instantiate two folders no ModellingRule permits, and a client following 5.3 to decide "is this Server doing AI" would browse for a folder that can never exist. The figure now shows the AI cluster where it actually lives - under AiRootType, beside the Vision object, not inside it. Both worked examples had the same fault and a worse consequence: they parked AI instances in those folders and never instantiated AiRootType at all, so the only executable demonstration of the new seam could not satisfy the AI-Base facet that VIS-Inference-* and VIS-Learning now require. They now build a proper AI root under the Server object. The learning loop lost content in the move that landed nowhere: the StatusCode table, the idempotency of StartCollection/StopCollection, TriggerTraining returning Accepted=false with Good, and the all-or-none rule for a null Deployment. Worse, two transitions vanished - Promoted had NO outgoing edge, so the loop could run exactly once, contradicting the AI spec's own "the cycle repeats" two clauses earlier. Restored, with the transition table it needs to be testable. Both ReferenceType arrows in the AI overview figure were drawn backwards against their own definitions. UsesModel is Deployment->Model and TrainedOn is Model->Dataset; the figure said the opposite, so an implementer following it would emit references that fail the exactly-one rule. Vision's equivalent figure had them right, so the two documents disagreed. Vision still owned a rule it had handed over: 5.11 restated the exactly-one UsesModel constraint verbatim, over a ReferenceType it no longer defines - visible in the mangled "An DeploymentType", which is what a mechanical rename leaves behind. Also fixed: a stale AiModelType in a descriptor that a generated annex was publishing, an AiDeployment reference shipping inside the NodeSet itself, an ObjectType count that said 25 against an actual 21, and a scope sentence still claiming Vision covers AI. 9.5.1 rule 1 required something no member could express - there was no modelled path from a pipeline to a learning job, and the folder that used to anchor one had just been removed. InferencePipelineType.LearningJob makes it executable. Writing it beside its type renumbered InferencePipelineType.Stop, so it is allocated at the true end of the id space instead; 0 moved. The encoding id counter was unguarded, so the 5009 freed by the moved structure would have been silently reclaimed by the next structure added - the same append-only violation the member ids were carefully protected from. Two validator blind spots, both proved by the reviewers and both now mutation-tested: the AI validator rejected any struct field documented as Type.Field, and the reverse cross-check silently SKIPPED a backticked owner that no model declares - so a reference to a retired type passed. That check now lives in Vision's validator, which is the only one with both models loaded and therefore the only one able to tell an outside type from a nonexistent one. Types from companion specifications are allowlisted explicitly, so taking a new outside dependency is a deliberate edit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa * Require a Mandatory member to be documented, not just its type The both-directions cross-check only compared type NAMES, so a whole Method and three Mandatory members shipped undocumented: a Server is obliged to implement them and a client has nothing to read. ListModels, ModelSourceType.SourceId, and EvaluationRunType's RunId, EvaluatedModel and Metrics. The forward direction now checks member granularity for Methods and Mandatory members, and stops there: demanding prose for every Optional member would produce padding rather than documentation. All five are now documented, including why ListModels takes a filter - a public catalogue holds more models than any client wants to page through, so a Method that could only return everything would be unusable against exactly the sources clause 8 exists to reach. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa * Let a Server actually use a model, not just describe one (#50) * Let a Server actually use a model, not just describe one The model said what a model IS and never let anyone use one. EndpointUri was a bare String: no wire contract, no credential, no capabilities, no failure semantics. A Server holding one knew where to send bytes and nothing about what shape they should take or what to do when the far end stopped answering - so "consume a model hosted elsewhere", which is the whole point of the InferenceLocation property, was unimplementable from what this model told it. Three additions. Clause 7 defines one invocation surface whose signature does NOT change with InferenceLocation. That is not an aspiration: serving runtimes that run on a workstation and the hosted services they mirror already expose the same contract, differing only in addressing and auth. The payload stays opaque - it is domain vocabulary - and what gets fixed is the envelope, where ModelUsed reports the model that ACTUALLY answered. A fallback or a followed reference moves it between the call and the read, so the deployment's current model is the wrong answer to "what produced this result", and wrong in the direction that looks plausible. Clause 8 makes remote consumption real: dialect, auth kind, a credential REFERENCE that is never a secret, capabilities, reachability, and the question no inference API answers because it can assume its caller will wait - what the line does when the WAN drops. FallbackPolicy is Mandatory. So is DataJurisdiction and EgressPermitted, because encryption answers who can read data in flight and not where it went. Clause 9 makes the catalogue an xRegistry domain extension rather than a private invention. Two things then fall out of the base type instead of being designed: ResourceType IS a FileType, so a staged artefact streams through the inherited Open/Read/Close; and it already carries ExternalReference, so a federated entry whose bytes live elsewhere is expressible without pretending to hold them. The import job federates by default and stages when the target could not otherwise reach the source, and verifies the digest at staging - the one moment a substituted artefact would enter. Jobs now derive from the Part 10 ProgramStateMachineType through a shared AiJobType, as Robot Intent does, so the transition events and auditability are inherited rather than reinvented. Costs, stated plainly rather than buried: this model now requires OPC UA - xRegistry, which spends the "exactly one RequiredModel" claim and moves the own namespace from ns=1 to ns=2. Both validators hardcoded ns=1 while reading it - Vision's would have resolved nothing and passed. Both now derive the index from NamespaceUris. No NodeId moved. JobId and LastError moved up to AiJobType and their ids are reserved, not reclaimed: 0 removed in place, 141 added, 0 moved. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa * Enforce the new rules, and show what they add up to The invariants that make clauses 7 to 10 executable rather than aspirational, each mutation-tested: - The four Mandatory members clause 8 rests on. A rule resting on an Optional member is a rule a conformant Server can silently not satisfy. - Invoke returns ModelUsed, Usage and FinishReason. - Nothing in the address space is NAMED like credential material. That is how the clause 8.2 prohibition gets violated by accident, and the address space is browsable, subscribable and historisable, so a secret placed here is not merely readable - it is archived. - The catalogue types narrow their inherited placeholders. A domain extension that leaves them open adds metadata while restricting nothing. - AiJobType still derives from the Part 10 base. - NamespaceUris order, because it fixes every namespace index in the file. All seven mutations caught; artifacts restored byte-exactly. Annex C works one arrangement end to end: a federated cloud model and a staged edge model, the WAN dropping, and the fallback answering. The point of it is the sentence a table cannot make - the client called the same Method with the same arguments throughout and never learned that inference had moved, except by reading ModelUsed, which is exactly what an audit a month later needs and what a client logging the deployment would have got wrong. ApiDialectEnum.OpenAiCompatible became RestChatCompletions. The literal named a company. A standard's enumeration should describe the contract, not advertise whoever published it first - and the prose says what it is compatible with without needing the name. No vendor is named anywhere in the document. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa * Deepen the clauses that were already there Adding three clauses left the existing ones describing a smaller model than the one that now exists, and put 5.6 between 5.1 and 5.2 - the out-of-document-order defect this repository has been bitten by before and which no check catches. 5.6 moved to where it belongs. Every heading is now in order. 5.2 documents what a model carries beyond its artefact: the publisher that completes the identity triple, and Quantization, which is not a packaging detail - a quantized model is a different artefact that gives different answers, and treating it as a variant is how a model evaluated at full precision ends up deployed at reduced precision with nobody re-measuring it. 5.3 says why Mixed is not a hedge, and that a dataset whose class list disagrees in ORDER with its model is undetectable: every identifier resolves, every count is plausible, every label is off by one. 5.4 gains a table of everything that makes a deployment callable, and the reason State and Reachability are separate. Ready + Unreachable is a network fault; Faulted + Reachable points at credentials. Collapsing them loses exactly what a commissioning engineer needs. 6 gains the joins to the consuming half - a trained candidate is not privileged over an imported model and owes the same lineage - and says plainly that almost no Server implements the whole loop, which is why State is read rather than inferred from which Methods exist. 11 grew because the attack surface did. The chain has more links; two break while a Server looks correct. The resolver policy now governs seven URIs, and applying it to a staging import AFTER transfer is not a control. Credential material is unaddressable, argued from the fact that the address space is subscribable and historisable - a secret there is not exposed once, it is published and archived. FollowsRef is promotion's second door and needs the same authorization, or the audit trail shows every promotion having been authorized. And a fallback may not be more permissive on egress than what falls back to it, or a network fault quietly sends plant data where policy forbids. 11.4 now says what DigestVerified actually means: the artefact is the one this catalogue entry described. An attacker who can edit both artefact and entry defeats it, and the catalogue is the better target because many machines read it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa * Require a Mandatory member to be documented, not just its type The both-directions cross-check only compared type NAMES, so a whole Method and three Mandatory members shipped undocumented: a Server is obliged to implement them and a client has nothing to read. ListModels, ModelSourceType.SourceId, and EvaluationRunType's RunId, EvaluatedModel and Metrics. The forward direction now checks member granularity for Methods and Mandatory members, and stops there: demanding prose for every Optional member would produce padding rather than documentation. All five are now documented, including why ListModels takes a filter - a public catalogue holds more models than any client wants to page through, so a Method that could only return everything would be unusable against exactly the sources clause 8 exists to reach. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa * Make the document read as a statement of the model, not its history Five changes, none of which alter what the model can do. The NodeId space had gaps at 6036, 6042 and 6104..6106 - residue of two members that moved to a base type and three placeholders that narrowed nothing - and the generator explained them with comments like "6036 held JobId, now inherited". A reader implementing this today should not be able to tell any of that happened. The model is unpublished, so the ids are now contiguous: 6001..6145, 145 members, none lost. The diff against the previous commit is a deliberate full renumber rather than the usual 0-moved. 1.2 "Why this is a separate specification" is gone. Its third paragraph duplicated 4.2 outright, its second defended the document existing, and only the first said anything about the model - which is now in 1.1 where the motivation lives. 1.3 also carried two bullets that both said training is out of scope. 11.1.1 was titled "The chain has more links than it used to", which is a changelog sentence in a heading. It says what the chain is. Five flows had no diagram: a call and what comes back, async submit and subscribe, the far end failing and a fallback answering, federate vs stage with the digest gate, and the provenance walk. Every arrow direction was checked against the generator's ReferenceType definitions rather than against intuition, because the last review found both arrows in the overview figure drawn backwards against their own. Annex D deploys an ONNX gearbox classifier end to end. Clause 7 had acquired some vocabulary from generative models - a capability called chat, accounting that is often tokens - while what industry runs is a fixed-shape tensor model executed in-process. The annex shows the same envelope carries it with nothing bent: tensor signatures as the entire interface description, a staging import with the digest verified, usage metered in samples rather than tokens, and Capabilities naming no chat capability at all - an absence that means nothing, because the list is open so a deployment describes what it does instead of scoring itself against a menu. It also says why no tensor wire format is standardised: raw buffers, protobuf, Arrow and npy are each right somewhere, and a Server already speaking one to its runtime should not transcode into a format chosen here. ContentType names which is in use, which is what a client needs. Plus a table of contents, 69 entries, all resolving. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa * Retitle: AI Model Management and Inference "AI Deployment and Learning" named two of thirteen clauses. The document also covers what a model is, calling it, calling one hosted elsewhere, the catalogue and bridge, and governance and provenance. Management carries the catalogue, import, lifecycle and governance; Inference carries the call, which is the largest clause and the first thing an implementer reaches for. The type system was already name-neutral, so almost nothing in the model changes. The namespace stays http://opcfoundation.org/UA/AI/ - it was never named for the old title, and changing it would invalidate every RequiredModel pin and every overlay reference for nothing. AiRootType, AiJobType, AiResourceType and the AI-* facets are all prefixed Ai, not AiDeployment, so they stand. Exactly one identifier carried the old title: the well-known browse entry point. AiDeployment becomes AiModelManagement, which is a BrowseName change - NodeId 7001 does not move. The CSV diff is 0 moved, with that one symbolic name replaced and nothing else touched. Folders moved with git mv so history follows the document rather than showing a delete and an add. The riskiest part was not the rename but what reaches into this tree by path: both Vision generators, the Vision validator, validate_all.py, the tree README and sixteen references in the Vision specification. A missed path there would not have failed loudly - the Vision validator returns empty sets when the AI NodeSet is absent and then passes - so AI_TYPE_ID was checked to be populated rather than inferring it from a green run. It resolves 42 types and 208 ids. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa * Address PR #49 review feedback Headings stated conclusions instead of naming topics, so a reader scanning the contents met forty arguments rather than a structure. Forty-three are now topic names - "Provenance", "Lineage", "Version binding", "Data residency and egress" - and every statement a heading was carrying has been restored as the opening sentence of the clause it belonged to, so nothing was lost in demoting them. The information model sections read as clipped one-liners. 5.2, 5.3 and 5.4 now open by saying what the type is for, who reads it and when, and the member descriptions say what each is needed for rather than only what it holds. The 5.4.1 table literally prefixed a "The question" column before each answer; it is prose now. 5.4.2 compared State with Reachability, which clause 8 does not introduce until much later - the reader met a comparison against something undefined. It now describes State on its own terms, including why Degraded sits between Active and Faulted, and defers the combination to 8.4 where the other half exists. "The bridge" was doing two jobs. A Server reaches an external AI system either by calling one per request (clause 8) or by obtaining a model from a catalogue once (clause 9), and both get called bridging. 9.2 now separates them before defining the import job, and the clause is titled "The catalogue and model import". "A card that lists only what a model can do is marketing" was polemic for a specification; it now says the same thing about behaviour and commissioning without the jab. The decision record explaining why this was a separate specification was already removed in an earlier commit; the section is gone rather than rewritten, since a standalone document does not argue for its own existence. Contents regenerated for the renamed headings: 69 entries, all resolving. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa * Address PR #49 review feedback: payload size and scenarios Invoke carried its payload as a ByteString, which is bounded three times over - by MaxByteStringLength, by the negotiated MaxMessageSize and by the Session's MaxResponseMessageSize - none of which this model gets to choose. An image, a point cloud or a window of high-rate samples exceeds them routinely, so the one call the specification defined could not carry the input it was defined for. Invoke is now the shortcut and BeginTransfer is the general path. InferenceTransferType carries one chunked exchange whose Request and Response are Part 5 FileType objects, so a client opens, writes or reads in chunks of its own choosing, and closes. Nothing here invents a transfer protocol: OPC UA already has one, every client implements it, and a bespoke chunking scheme would be a second thing to get wrong. MaxInlinePayloadSize is Mandatory and says the bound before a client calls rather than after a rejection, because the three limits above are not all visible from the client side and the smallest of them is the real one. Clients that do not know their payload sizes in advance are told to start with the transfer path. The awkward case is not a large request - a client knows its own input - but a request that fits producing a response that does not. Invoke returns TransferRequired and Transfer for that, and a Server shall NOT fail the call: the inference already ran, and failing would discard the work and tell the caller nothing, leaving it to read an empty payload and conclude the model returned nothing. Streaming stays where it was. A transfer delivers one complete response however large; 8.5 covers output a client wants as it forms, over a Subscription or optionally a data channel. Clause 4 Scenarios is new, and informative: eight arrangements this specification supports, each with the flow that realises it, so a reader can find the one resembling their installation before reading the normative clauses that define its parts. They are not alternatives - a plant running a model locally, calling a hosted one for a second opinion and importing both from a catalogue is doing three at once. Clauses 5 to 14 shift by one to make room. Every clause reference was re-checked by hand rather than trusting the section-ref gate, which resolves a bare reference against any cited document and would not have caught a stale one. 0 NodeIds moved; 22 added. * Show how the AI model maps onto the systems people actually integrate (#54) * Add vendor implementation guides for the AI Model Management specification The specification names ApiDialectEnum literals for what a contract does rather than for whoever published it first, because a literal in a standard should not be an advertisement. That is right for the normative document and leaves an implementer holding a question it does not answer: they are not integrating a REST chat-completions contract, they are integrating Azure AI Foundry, or Bedrock, or the Triton server already in the plant. Eleven informative guides under extras, one per system, each with the same nine sections so the index's coverage table means something. The ninth - what this system does not tell you - is the one worth reading first; a mapping document that lists only what maps is marketing. Two findings the set makes hard to avoid noticing. No hosted inference platform in it exposes an artefact digest. Not Foundry, OpenAI, Bedrock, SageMaker, NIM, Triton or Vertex. Identity everywhere is a name string. Hugging Face is the only content-addressed system here and it serves no inference. So Digest stays empty against a hosted endpoint, a Pinned deployment is pinned to a NAME with the provider's policy rather than a hash holding it still, and the Stage import mode is the only path in the set that ends with a digest the Server computed itself. AuthenticationKindEnum cannot express AWS SigV4. The guides map it by what is STORED, which is what makes the member answerable across systems whose handshakes have nothing in common: an IAM role is WorkloadIdentity because no secret exists anywhere, static access keys are ApiKey. Whether the enumeration should gain a literal is a separate decision - it would be a NodeSet change - and is not taken here. validate_examples.py re-derives from the NodeSet and the specification, never from the guides, and is wired into validate_all.py because a validator outside the aggregate silently stops running. It checks that cited members exist, that every dialect and authentication literal is exercised, that conformance units are real, that each guide links the specification by relative path - which is also what makes a bare section reference resolve, metaverse-specs being a strict tree - and that the index agrees with the guides. It earned its place twice while being written: it found that no guide covered OpcUaInference, which is why there is now one for federating to another Server implementing this specification, and it caught a member this author had cited as ImportMode when the model calls it Mode. Mutation-tested against all five checks. No model file is touched: no NodeSet, no NodeIds CSV, no generator, so no version bump. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa * Correct the vendor guides against an audit of all eleven Nine of the guides were written by sub-agents and only spot-checked. A three-way audit against the sanctioned research and the specification found errors in every category the audit looked for, including in the two documents the lead wrote. Conformance claims that were simply wrong: AI-OffServer requires EndpointUri to name an authenticated, confidential scheme. The NIM, Triton and KServe guides all claimed it while showing a plain http:// listener as the default - which is the ordinary way a self-hosted inference server sits on a plant network, so this was wrong in exactly the case a reader would be in. AI-Import requires AI-Catalogue; an import job with nothing to import from is not implementable. SageMaker and Triton claimed AI-Import from S3 URIs and a model repository respectively, neither of which is a catalogue that declares a digest to verify against. The federation guide claimed every conformance unit an upstream Server implements is reachable through the link. A conformance unit describes what THIS Server exposes; reaching a Server with a catalogue does not give this Server AI-Catalogue, mirroring it does. Rewritten as three lists by what the downstream Server actually has to build. Model errors: ApiDialect is Mandatory, and the Hugging Face guide left it empty on the grounds that a catalogue speaks no inference contract. True, and not available: it is Proprietary with EndpointDescriptionUri doing the work, and the guide now says the cleaner arrangement is to model the Hub as a ModelRegistryType under clause 10 rather than bending a ModelSourceType around it. AI-Invoke requires ModelUsed populated on every response, so the federation guide's suggestion that a downstream Server may return null when it cannot resolve the upstream model was not one of the options. It publishes a ModelType for the substitute or it fails the call. RateLimit.RetryAfter is a Duration; Retry-After carries seconds or an HTTP date. Converted, not copied. Citations: ModelUsed is 8.2.1 and not 8.5, which is Incremental results; the payload bound and the transfer path are 8.2.4 and not the whole of 8.2. The index attributed the digest rules to clause 11 when 12.1 states them, and asserted a shall-not the specification does not contain - what 12.1 actually requires is that a Server populate Digest where the artefact is obtainable through ArtifactUri, which for a hosted model it is not. Unsupported vendor facts, removed rather than softened: ONNX file metadata fields, GGUF header contents, Triton's storage backends and repository-management extension, and Retry-After attributed to specific platforms rather than to HTTP. Triton's dialect split was a transport split. Clause 9.2 names the literals for the contract, and Triton serves OIP over both HTTP and gRPC, so both are OpenInferenceProtocol. TensorRemoteProcedure stays, described as what it is for. The index also called Hugging Face the only exception to no artefact digest, omitting embedded runtimes - the one arrangement in the set where the digest is something the Server computed rather than was told. Comparative praise replaced with description throughout. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa * Restore the blank line before a heading in the federation guide Lost while rewriting the catalogue section for the conformance-unit audit. MD022, and the only markdownlint finding across the set. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa * Releases 0.3.0 and 0.4.0: what eleven real systems taught the AI specification (#55) * Integrate what the vendor mappings taught the AI specification Mapping one information model onto eleven real systems was a natural experiment, and it found places where the model was unsatisfiable, unguarded or ambiguous. None of them needs a NodeSet change. AI-Invoke was unsatisfiable for every non-language model. The facet required Usage "populated on every response" while no clause imposed that obligation, and "empty" was unencodable: the three counts are UInt64 and the model declares no optional fields, so leaving them empty encodes as zero, indistinguishable from a metered zero. Five guides claimed the facet while documenting empty Usage. 8.2.3 now makes an empty UnitKind the not-metered sentinel, with the counts set to zero and clients forbidden from reading them as measurements. The unit carries the sentinel because the counts cannot. Residency did not compose across a federation hop. A payload leaves a deployment along exactly two modelled edges, FallsBackTo and Source, and only the first was guarded; EgressPermitted's only shall binds on InferenceLocation Cloud, so a cell-to-site EdgeOffServer hop escaped it. 9.5 states the members are end-to-end rather than next-hop, and obliges a Server federating to a peer to read its declarations and never publish anything more permissive, defaulting fail-safe where it cannot read them. This is the rule 12.3.2 already states for the other edge. Clause 13.1 described a plant MLOps node that "may never call Invoke at all" while AI-Base required at least one deployment. The facet's deployment requirements are now conditional on the Server exposing one. AI-OffServer cites 12.2's scheme requirement instead of restating it, so the two cannot drift. Three ambiguities are resolved where the guides had each invented an answer. ModelId carries the source's identifier verbatim, and Publisher names who produced the model rather than who serves it -- two Servers were reading owned_by into Publisher and publishing "azure" for a model Meta trained. AuthenticationKind classifies what is stored rather than the handshake performed, which is what makes SigV4 answerable without a new literal. ApiDialect classifies the contract this Server speaks, and a catalogue-only source is Proprietary with an EndpointDescriptionUri -- correct, not a shortcoming. The eleven guides are updated to cite these rules rather than derive them, and three that had softened 12.2's shall into an operator choice are corrected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa * Release 0.3.0: state why a digest is absent, and reach a registry from an import Three changes the vendor mappings argued for that prose alone could not make. All three append; NodeId churn is 0 changed, 0 removed, 5 added. DigestProvenanceEnum, Mandatory on ModelType and Optional on ModelResourceType. Digest is Mandatory so that its absence is uniform and browsable, and that stays right -- seven of eleven systems cannot fill it, and Optional would make "no digest" indistinguishable from "does not implement digests". But empty then carried two meanings and populated carried three, and the missing answer was doing visible harm: five guides each prohibited a DIFFERENT wrong value -- a response fingerprint, a resource name, a storage entity tag, a manifest hash, a commit identifier -- and none could say what to publish instead. 12.1.1 states the prohibition once and supplies NotAvailable. Mandatory for the reason Digest is, which 6.2 already gives: clause 12 depends on it, and a rule that depends on an Optional member is one a conformant Server can silently not satisfy. A StatusCode cannot carry it -- DeclaredBySource and VerifiedOnStage are both Good. 12.1.1 also states that provenance does not strengthen by being forwarded, on the ordering NotAvailable < DeclaredBySource < ComputedByServer < VerifiedOnStage. Republishing an upstream peer's VerifiedOnStage would claim a check this Server did not perform, and the claim would be indistinguishable from one it had. Same composition argument as 9.5 residency, applied to the other thing a federated deployment forwards. ModelImportJobType.Registry, with 10.2 requiring exactly one of Source and Registry non-null. Source was Mandatory and typed ModelSourceType, so nothing connected an import job to a ModelRegistryType -- the edge the 4.4 and 5.1 diagrams draw. A Server importing from a catalogue had to invent a source wrapper around its own registry. Source keeps its Mandatory rule and takes null on the registry path, the idiom TargetDeployment already uses, so no existing member changes. ModelType.Name is constrained rather than retyped: its Text shall be the source's name carried across unchanged, translations may be added for display, and the Text is not reformatted. Retyping to String would have been cleaner and is breaking; the property wanted -- two Servers fetching one model from two mirrors produce the same string -- is obtainable by constraining the field. Version and PublicationDate bump to 0.3.0 / 2026-08-05, picked up by the vision overlays automatically because build_examples.py derives both from the AI generator rather than pinning them. The eleven guides state their DigestProvenance value and cite 12.1.1 instead of each re-arguing the prohibition. The index reads the member as the axis the set varies along: evidence tracks custody, which is why an embedded runtime -- a local file the Server can simply hash -- is the one arrangement here that produces evidence without vendor cooperation. Adds CHANGELOG.md, where this repository keeps release history so the specification does not have to narrate it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa * Stop sending a homeless identifier to a member that cannot hold it 12.1.1 correctly forbids a non-content identifier in Digest and then disposed of it into ArtifactUri or ProvenanceUri. Those are scalar String Properties on ModelType -- one artefact, one value. A serving configuration fingerprint is per-response and per-deployment, so the disposition is unimplementable for the case it was most needed in, and five guides were left telling an implementer not to publish a value the specification had just promised a home for. The narrowed rule says what is true: an identifier that LOCATES the artefact or its provenance record belongs in those members, and one that identifies something else has no member in this model. Recording that a datum has no home is better than filing it where a client reads it as an artefact digest. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa * Release 0.4.0: model what eleven real systems publish and this had no member for The same eleven mappings, read with a different question. 0.3.0 asked which rules were wrong; this asks what these systems do that the model cannot represent at all. The answer had a consistent signature: a guide NAMES a vendor field and then maps it nowhere, and the omission is invisible because nothing is missing from the guide's own tables. Twelve appended Properties, no new DataTypes. NodeId churn is 0 changed, 0 removed, 12 added. A model had no time on it. Six of eleven return a vintage in the call a Server populates ModelType from, and every guide dropped it. The asymmetry made it plain: DatasetType.CreatedAt exists with 6.3 defending it, and a catalogue resource inherits CreatedAt/ModifiedAt from xRegistry, so a model known through a catalogue had a vintage and the same model federated from an endpoint did not. LastModifiedAt is the load-bearing one: 9.3's FollowsRef lets the artefact change with nothing else changing, and 12.3.1 requires repointing to be an authorization-bearing act while pointing at a job record that a source-side move never produces. A model had no end to it. Bedrock alone publishes legacyTime and endOfLifeTime -- one vendor of eleven, and in the model anyway, because vendor count is the wrong measure for an industrial specification. On that date the deployment does not degrade, it stops; FallbackPolicy fires, and where that is FallBackTo the line keeps producing while something outside the qualified configuration answers. Every other availability facility here copes after the fact. This is the only one whose value is a date in the future. The asynchronous path could not carry a large payload. InferenceJobType had no TransferRequired and no Transfer, so InvokeAsync was bounded by exactly the limits 8.2.4 says this model does not choose -- while 8.6 motivated it with the requests most likely to exceed them, and 13.2 claimed a parity that did not exist. 8.6.1 separates two problems that had been collapsed: a result too large to carry, which the transfer pair solves, and data that never needed to move, which it does not. Five guides had discarded the vendor URI mechanism on the ground that it is not a chunked transfer; they were right, and there was nothing else to compare it to. A deployment did not say what to send it. Ten of eleven publish a request contract; the model captured the tensor half only, which is why AI-Signatures was unclaimable by every hosted platform. The vocabulary existed and was out of reach behind an explicit assertion in 9.2 that these members never affect how a client calls this Server. 6.4.2 puts ApiDialect and EndpointDescriptionUri on DeploymentType and 9.2's sentence is rewritten rather than quietly dropped: naming which contract opaque bytes must satisfy is not typing them, and 8.2's argument for opacity is about contents. A pinned deployment could move without anything changing. 12.1.1 forbade putting a serving-configuration fingerprint in Digest and offered ArtifactUri or ProvenanceUri, which are scalar Strings on ModelType and cannot hold a per-deployment value, so the datum was thrown away. RuntimeIdentity holds it, and 9.3.1 states what makes it matter: a change to it IS the observable change to the deployment that 9.3 says a pinned artefact cannot move without. Three defects found by reading the specification against itself. 6.4.3 made Degraded normative for a deployment missing its LatencyBudget while no measured latency existed anywhere, so the rule could not be observed to be satisfied or violated by any legal Server; ObservedLatency supplies the input. 13.2's AI-InvokeAsync row claimed size parity it did not have. 9.4's ListModels answered catalogue size with a cap and no cursor, putting everything past MaxResults permanently out of reach. Nothing added is Mandatory. Each is governed by a conditional shall in the clause that defines it, because a blanket Mandatory would oblige nine of eleven Servers to publish a RuntimeIdentity they cannot obtain, and a requirement no conformant implementation can satisfy is not a requirement. Breaking: Invoke and InvokeAsync gain a PayloadUri input and ListModels gains a ContinuationPoint in and out. Argument lists are the Value of an existing Variable, so no NodeId moves, but a client that calls positionally will break. Two tooling fixes fall out. validate_examples.py now reads Method argument names from the NodeSet, so a guide citing PayloadUri is checked rather than suppressed -- mutation-tested against a typo and an invented name. And a member description holding paragraphs was ending its own table row in the generated model reference, which stayed invisible only while such a member happened to be last in its table. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa * Define the profiles both clause titles have promised since 0.1.0 Clause 13 of the AI specification and clause 12 of Robot Intent are both titled "Profiles and conformance units" and both defined facets only. Neither information model changes, so neither version moves: profiles publish through the base-UA ServerProfileArray and need no member. A facet is a building block; a profile is a complete claim. The distinction is commercial rather than technical -- a plant writes a profile name into a purchase order, and enumerating nine facet names correctly is not something a procurement document does reliably. Robot Intent's 1.2 already depends on this without a word for it: a mixed-fleet cell works only if two robots claim the same shape. Four AI profiles. Inference Device, Inference Gateway, Model Catalogue, Model Lifecycle. The first three were already in 13.1 as prose examples -- a device running one fixed model, a gateway calling a hosted one, a plant MLOps node that may never call Invoke -- so naming them cost nothing but a name, and 13.1 now points at 13.3 instead of restating them. AI-Residency is INSIDE the gateway profile rather than optional to it. Once inference leaves the Server, where does my data go has an answer, and a gateway that cannot state it is the arrangement 9.5 exists to prevent. The Lifecycle profile is witnessed by none of the eleven mapped systems and is defined anyway. All eleven are inference or catalogue systems; none is a plant that trains, which is what clause 7 was written for. Its absence was also why AI-Learning, AI-Dataset and AI-Stream looked orphaned -- they belonged to a Server shape the document described nowhere. AI-Stream stays in no profile, because 13.2 already says a Server answering only through Invoke is conformant without it and a facet genuinely optional to every shape should not be bundled into one. Four Robot Intent profiles: Motion, Handling, Path, Mission. RI-Safety sits in the baseline, which is the decision here most worth arguing about. Clause 10 is explicit that nothing here is safety-rated and no Method is a safety function; what it imposes is a duty to report what the safety system enforces and refuse work exceeding it. An integrator specifying a profile is entitled to assume a robot declines an intent its safety configuration forbids rather than attempting it. The process facets are deliberately in no profile: a welding robot is a Path Server that additionally claims RI-Process-ArcWeld, and bundling the process in would have produced one profile per process with no way to say the motion underneath is the same. Both documents gain URI tables. A profile name is for a human and ServerProfileArray holds URIs, so without stated values two Servers implementing one profile publish different strings and no client matches either. Both gain definitions for conformance unit, facet and profile. Each used "facet" throughout and defined it nowhere, and in the AI document "profile" collides with the typed profile of a consuming specification, which is a payload vocabulary and unrelated to conformance. Profiles are added AFTER the facet tables. 13.2 is cited fourteen times; moving it would have churned every reference for nothing. validate_examples.py gates profile names against the profile table, so a guide naming a shape the specification does not define fails the way a misspelled facet does. Mutation-tested against a typo and an invented name. It checks the name and not the claim, which its docstring says plainly: whether a guide's facets cover the profile it names is a judgement over prose, and a checker guessing at that would fail on wording rather than substance. The eleven guides name their profile. Eight reach Gateway, two reach Device, one reaches Catalogue, and none reaches Lifecycle. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa * Separate a paragraph from the table it followed The ServerProfileArray obligation sat on the line after the last facet row, so markdownlint read it as a fourteenth row with one cell. My pre-push check tested for a table glued after text and not for text glued after a table; it now tests both directions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa * Say what this repository actually contains The README explained how to contribute well and what exists badly. The inventory was a Layout list of single-paragraph bullets -- the Data Channels one ran to 1,900 characters -- carrying no version, no status, and no link to any specification document. A reader could not get from the README to a specification in one click, could not tell which of the sixteen documents were under OPC Foundation review, and had no single place saying what the whole effort is. It is now one table per folder prefix, covering this repository and the private submodule together, so the grouping is complete rather than split by which repository a document happens to sit in today. That is why wot appears at all: both its specifications are under review and the tree has left, so a prefix-grouped list is the only place the reader learns they exist. Columns are Specification, what it is and why it exists, status, and documents -- markdown first, then Word. Three specifications have no Word rendering and their cell says so with an em dash rather than omitting the column, because the gap is worth seeing: Vision, Robot Intent and AI Model Management have no clause map under word-drafts/tools/specs/, which is what drives that build. release_spec.py gains a third repair pass. It maintains this file when a specification moves private -- capsuling the describing list item with its original base64-encoded so a return restores it, and emptying links into the moved tree -- and it understood list items and links, not table rows. Without the pass the next release would leave a row advertising a document that is no longer here. Three things had to be right and were verified rather than assumed. The row pass runs BEFORE the link pass, because afterwards every row already holds a capsule around its own links and would be skipped, exactly as the bullet pass skips such lines. The replacement keeps the row's cell count, since collapsing four cells into two fails MD056. And only tables whose last two columns are Status and Documents are touched, so a table of another shape is left alone rather than rewritten into something the function guessed at. Return needed no new code: capsule_belongs_to already matches a capsule by a link resolving into the returning set, and every row carries one. Round trips for six specifications across four trees restore the file byte-for-byte. The five capsules for the specifications already under review encode the table row they should become, not the bullet they used to be. Restoring a 1,900-character bullet into a table cell would be worse than the gap it filled. The five reviewed specifications link to github.com/OPCF-Members rather than to spec-drafts/ paths. The submodule is empty for everyone who is not a member, so a relative link would fail check_links.py for them and still open nothing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa * Keep the capsule markers inside the table row's outer pipes markdownlint counts a row's cells without stripping HTML comments, so a closing marker after the final pipe leaves the row ending in content: MD055 read it as a missing trailing pipe and MD056 counted a fifth cell. Both markers now sit between the outer pipes, which is invisible to a reader and correct to the parser. My pre-push check stripped the comments before counting, which is exactly why it passed locally and failed in CI. It now counts them the way markdownlint does. Round trips still restore byte-for-byte; the encoded original is the row between its outer pipes rather than after the first one. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa * Trim the blank line at the end of the README MD012 counts the newline after the final paragraph plus the file's own terminator as two blank lines. My check read the file through Get-Content, which drops that distinction, so it saw one. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa
# Description Implement the draft **OPC UA — Robot Intent** companion model end to end: task-level motion verbs, a Part 10 lifecycle, a server hosting surface, a client, two samples and a live integration suite. It replaces the opt-in convention API merged in #4127 with an implementation generated from a companion information model. ## Why OPC 40010-1 *Robotics* describes robot **topology** in detail and defines **no motion verbs at all** — its entire actuation surface is `Start`, `Stop` and loading a named program. A conformant client can discover everything about a robot's construction and cannot ask it to move anywhere. #4127 filled that gap with ten verbs in an application-owned namespace, resolved by BrowseName, and the file said what it was: *"opt-in, explicitly non-normative industrial operation conventions"*. That contribution established a **vocabulary**. It established no **lifecycle**, and the lifecycle is the harder half. A motion takes seconds; a pick takes a minute. OPC 10000-4 §5.12.2 discards a method result when the Session ends *"independent of the task actually performed at the Server"* — so a synchronous motion method does not merely time out, it **loses the outcome of work that has already physically happened**. OPC 10000-10 §4.1 gives the OPC Foundation's own resolution: a Method performs a calculation, a **Program** runs a batch process or a machine tool part program. So `SubmitIntent` returns as soon as the intent is *admitted*, and what it returns is a NodeId — an `IntentOperationState` (a Part 10 program instance) the client subscribes to for progress and reads for the result. ## What changed **Model and shared code** — `Opc.Ua.RobotIntent.NodeSet2.xml` is source-generated in `Opc.Ua.Robotics`, so the enums, the polymorphic intent structures, `IntentOperationState : ProgramStateMachineState`, the method states and the typed clients derive from the model rather than being hand-written. Its only `RequiredModel` is the base UA namespace, so a server can adopt it without OPC 40010 or DI. `PoseMath` implements the specification's Annex C conversion between its unit quaternion and the core `ThreeDFrame` — including the `asin` clamp that stops a pole orientation becoming a domain error, and the non-negative-`w` representative that makes two servers agree on four numbers. `FrameTree` composes transforms along the frame tree. **Verbs are a DataType hierarchy, not one Method each.** A single submission and a mission step are then the same shape, and a new intent is a subtype rather than a new method — which is what `AddOperation<TRequest,TResponse>` was working around. **Server** — `IntentControllerHost` owns admission in the specification's order, the queue with PLCopen buffer modes, blending, cancellation *with the server's right to refuse*, missions with a committed base and a revisable horizon plus an IEC 61131-3 step graph, safety-aware refusals, real-time channel brokerage, and the capability declaration. All thirteen Methods are wired; command authority and channel leases are released when the holding Session closes; every client-supplied NodeId is validated against the controller being commanded (clause 11.3). `RobotIntentNodeManager`, the fluent `IIntentControllerBuilder` and `AddRobotIntent`/`ConfigureRobotIntent` make it reachable, and `RobotIntentFacetCalculator` computes which of the specification's facets an instance actually satisfies. **Client** — `RobotIntentClient` discovers `Server/RobotIntent/Controllers` and reads what a robot accepts; `IntentOperationHandle` subscribes and completes on a terminal state, recovering the result after a reconnect because clause 6.7 promises it survives; `CommandAuthorityLease` and `RealTimeChannelLease` manage the two exclusive resources; fluent builders cover every intent and mission. **Samples** — all robotics samples move under `samples/Robotics/`. `IntentEnabledRobot` is one UR5e-style offset-wrist arm on a bench, declared completely enough to show what a conformant server publishes, with a simulated safety source so the clause 10.4 refusals are demonstrable. `IntentViewerClient` turns a **click on a target prim in the OpenUSD viewport** into an intent and watches the arm execute it; it also runs headless, which is how CI exercises it. **Intents execute serially**, which satisfies every `BlockingMode` constraint by construction: the specification forbids beginning a `Single` or `Hard` intent while another executes and merely *permits* `None` and `Soft` to overlap. The convention API (`RoboticsOperationConventions`, `RoboticsOperationsBuilders`, `RoboticsOperationsClient`) is removed. ### On safety — what this deliberately does *not* claim The interface is **non-safety-rated**, and that is a property of the technology rather than a scoping choice. OPC 10000-15 carries cyclic safety data from a SafetyProvider to a SafetyConsumer; the consumer's `RequestSPDU` holds an identifier, a monitoring number and one octet of explicitly **non-safety** flags, so a caller has no channel through which to supply safety-rated arguments. Every safety fieldbus (PROFIsafe, CIP Safety, FSoE, openSAFETY) expresses a safety command as a **continuously asserted cyclic signal**, because the integrity argument rests on the fail-safe state that follows when assertion *stops* — and a Method call has no defined behaviour when it stops being called. What the host does instead is **observe and refuse**: `UpdateSafetyState` is how the application reports what the safety system is enforcing, and admission then refuses on the same values a client can read, so a refusal is explainable from the address space rather than from Server-internal state. It may never *instruct* the safety system, and no Method here commands a safe motion function, changes an operational mode or clears a stop. ## Defects this found in the specification, fixed upstream Implementing a specification is the only way to find out whether it can be implemented. Seven defects went back to [marcschier/opcua-drafts#47](marcschier/opcua-drafts#47) rather than being worked around here: - **Refusal was unobservable.** Clause 6.2 orders six distinct refusals and clause 5.8 calls the failure set "small and diagnosable on purpose" — but `SubmitIntent` returned only `IntentId` and `Operation`, so a client could see *that* something was refused and never *which*. It now returns `Accepted`, `Failure` and `Message`, and clause 6.2 states that a refusal returns `Good` rather than substituting a Bad `StatusCode`. - **The Part 10 promotion was illegal.** `IntentOperationType.ProgramDiagnostic` was declared as a Property of `PropertyType`; OPC 10000-10 declares it a Variable of `ProgramDiagnostic2Type` reached by `HasComponent`. The declaration added a *second* member beside the inherited one instead of promoting it — which is precisely why an earlier version of this branch had deleted it. Correcting the TypeDefinition made it generate cleanly with **no change to the curated Core**. - **Clause 9 said nothing about the Method surface.** A server could declare `MissionsSupported` true and omit `SubmitMission` entirely, because the mission and channel Methods are Optional. This implementation did exactly that until an integration test caught it. A fourth honesty rule and a table now fix which Methods each declaration implies. - **Conformance was not machine-readable, and two implementations disagreed.** Clause 12 defined conformance in terms of facets and gave a Server nowhere to publish which ones it satisfied, so every client had to re-derive Table 12.2 from the address space. This repository proved why that fails: the server''s `RobotIntentFacetCalculator` and the client''s `RobotIntentRules.DeriveFacets` each implemented clause 12.2 independently and **disagreed on essentially every row** — the client''s `Blending` was `BlendingSupported` alone where the server also requires all four blending buffer modes, its `QueuedIntents` was `MaxQueueDepth > 0` where the server also requires `Buffered`. A client and server from the same release answered "does this controller conform?" differently, and both were reading the specification correctly, because several rows are behavioural and no amount of browsing settles them. `IntentCapabilitiesType` now carries **`SupportedFacets`**, exactly as OPC 10000-5 does with `ServerCapabilitiesType.ServerProfileArray`, RI-Base requires it, and clause 12.2 separates structural requirements a Server shall meet before listing a facet from attested ones governed by clause 9. - `WaitIntentDataType.Signal` was unbounded where clause 11.3 has to check it; §11.3 gained a table fixing the expected type of every NodeId-valued member; §5.7.0 gave `Ready`/`ActiveIntent`/`ActiveMission` normative meaning; §6.9 bounded `RequestedLease`; §6.5 and §6.4 now say what a server that cannot differentiate `StopMode` must do, and which stop a superseded intent gets. ## Bugs the tests and reviews caught Each of these would have been silent in the field: - **The execution engine was never wired into the public API.** `IntentControllerBuilder.RegisterAsync` built the address space and never started an `IntentControllerHost`, so through the documented `AddRobotIntent` → `ConfigureRobotIntent` path **every Method returned `BadNotImplemented`** and the registered `IIntentExecutor` was never resolved. A browsable address space is not a working robot interface, and only a live integration test showed the difference. - **NodeId scoping failed open on an empty index.** A controller advertising `CallProgramIntentDataType` with no published `ProgramType` accepted an *arbitrary* NodeId as `CallProgram.Program` — and `CallProgram` runs code the server holds. - **Blending completed the predecessor at submission time** and abandoned its motion, telling the client it succeeded and publishing an all-zero pose at the origin as a genuine `AchievedPose`. - **Mission base immutability compared only identifiers**, so a client could keep the ids and swap the *motion* of an already-committed step. - **`FinalResultData/Result` was never registered** with the node manager: Browse showed it, Read returned `BadNodeIdUnknown` — so clause 6.7's promise to a Part 10 client did not hold. - `DisposeAsync` disposed primitives out from under in-flight executor code; re-using a terminated `IntentId` destroyed the retained result and duplicated a NodeId; neither side registered the Robot Intent encodeables, so `Pose3DDataType` did not decode; host startup was hooked to a task that ran in only one hosting shape. - The result was published **after** the state went terminal; capabilities were resolved against a stale namespace table; an empty `ContentFilter` arrives as an empty element array rather than a null filter, so testing only for null made every unconditional mission transition silently untaken — a mission would run its first step and stop. ### Found by a later test-quality audit, after the above was written An audit of the test suite itself found five tests whose **names** asserted a normative requirement while their **code** asserted something weaker or nothing at all. The worst **fabricated the answer it checked**: a helper caught a decode failure and invented "5 supported intents, 6 axes, all facets true", so six capability-honesty assertions across four tests could pass while reading nothing from the server. The fallback is gone and the suite passes with honest reads. Chasing the facet gaps it reported led to the clause 12 defect above. Two further defects followed from completing that work: - **`RI-Interop-40010` was never computed**, and the code documented the omission as impossible to fix "because the calculator receives only a Robot Intent controller, not the linked OPC 40010 model". That was wrong: Annex B's structural requirement is an **inverse** `HasIntentController` reference, reachable from `IntentControllerState` — and the integration suite was already browsing exactly that. A server implementing Annex B could not claim Annex B. - **The published claim could go stale.** `SupportedFacets` was a snapshot taken during `RegisterAsync`, so a reference attached afterwards left it wrong. It is now bound through `BindRead` and recomputed on read, so the claim tracks the address space by construction rather than by discipline — which matters for anyone bolting Robot Intent onto an existing OPC 40010 node manager, where the attach ordering is often not theirs to choose. Two tests that **could not fail** were also replaced: one asserted `AccessLevel` equals `CurrentRead` to pin a read-only marking, but `BaseVariableState` initialises every variable to exactly that, so deleting the marking left it green; it is now a write expecting `BadNotWritable`, including a real OPC UA `Write` service call over a live session. Finally, a build break that a plain build hides: `RoboticsAotTests` used `out string?` in a project declaring `<Nullable>disable</Nullable>`. Building the project **without** `-f` suppresses `CS8632` through `NoWarn=nullable` and succeeds; building it **with** `-f net10.0` resolves `NoWarn` differently, `CS8632` fires, and `TreatWarningsAsErrors` makes it an error. CI builds per-TFM, so this would have failed there while passing every local build to that point. ## Testing and coverage | | | |---|---| | `dotnet build UA.slnx -t:Rebuild` | **0 errors, 2 warnings** — both pre-existing `CA1861` in `tests/Opc.Ua.Tools.Tests`, a file this PR does not touch | | **Whole `UA.slnx` test suite, net10.0** | **50 of 52 assemblies fully green** (measured at `40d9cf213`, before the `master` merge; the other two are explained below) | | `Opc.Ua.Robotics.Tests` | **467** net10.0, **447** net48 | | `Opc.Ua.Robotics.Intent.Tests` (live client <-> server) | **20** on net10.0 and net48, run repeatedly | | `Opc.Ua.OpenUsd.Tests` | **904** after merging `master` (664 before; the rest arrive with #4160 / #4162 / #4180) | | `Opc.Ua.Aot.Tests` | **121/121** | | `Opc.Ua.Di.Tests` (regression from the sample move) | **364/365**, one pre-existing skip | | **Changed-line coverage** | **90.7 %**, no file below 80 % — comfortably above the enforced band, which after #4158 applies only to patches larger than 100 changed lines | The two non-green assemblies are **not** caused by this branch. Rather than assert that, I ran the same selections at the merge-base `ae241b662` on the same machine: - `ServiceCallReassemblerTests.PrivateRequestSummaryFormatsKnownRequestKinds` expects `12.5` and gets `12,5` — a culture-sensitive assertion failing on a European locale. Fails 4/4 on **both** commits. - `CertRotationLiveTests.L2Cert2RotateCertificateDuringServerRestartRecoversSharedChannelAsync` fails on a teardown count. Fails 4/4 on **both** commits. - A third failure seen once, AOT `FindServersAsync` with `BadSecureChannelClosed`, did not reproduce: 4/4 on retest and 121/121 in the final run. Environmental. Two things are worth calling out because they would otherwise be invisible: - `tools/**` is **not** in the `coverage-thresholds.json` ignore list, and no test project referenced the OpenUSD tools assemblies — so their changed lines counted as uncovered and the real patch figure was 75 %, exactly the blocking threshold. Rather than widening the ignore list, the pure decisions in the viewport host (pointer-to-physical-pixel conversion, `RenderPickRequest` construction, stale-retry, backend discovery, pick-mode selection) were separated from the Avalonia/native shell and tested headless against a fake picking backend. - `dotnet build UA.slnx` **hides warnings on unchanged projects**; only `-t:Rebuild` surfaces them. That is how a `CA2263` introduced by a net48 portability fix survived several "zero warning" builds. The multi-target requirement earned its keep repeatedly: it caught a default interface implementation, `ArgumentNullException.ThrowIfNull`, `string.Create` with an interpolated handler, `ValueTask.FromResult`/`CompletedTask`, `Task.WaitAsync`, generic `Enum.GetValues` and generic `Enum.IsDefined`, and a non-generic `TaskCompletionSource` — none of which the net10.0 build complained about. A net10.0-**only** hang in the simulated executor and two load-sensitive flaky tests were also found and fixed at the cause. ### Merged with `master` `master` (through `e73e71184`) is merged in as `08205d81d`. Four conflicts, all where the two branches touched the same robot and OpenUSD surfaces, and none resolved by dropping a side: - #4162 added six files to `samples/MinimalRobotServer/` while this branch moved that directory to `samples/Robotics/MinimalRobotServer/`. They are kept at the moved path, and `UA.slnx` takes the union of both sides' new projects. - #4160 replaced `object` with a `UsdValue` union and rewrote `Flatten`; this branch had reformatted the same method and fixed a path in its doc comment. Git produced a hybrid that did not compile. The resolution keeps `master`'s `UsdValue` arm and `Flatten(UsdValue)` overload in this branch's block-bodied form, with the path fix retained. - `master` inserted `FindStageCamera` directly before two methods this branch had relocated, so the merge duplicated `WriteStageUsda` and `GetPrivateStateRoot`. Only `FindStageCamera` is genuinely new — `master`'s diff shows the other two as unmodified context — so the duplicates are dropped and this branch's `internal` versions kept, including the `GetPrivateStateRoot(string?)` overload the connector tests use to stay out of the real user profile. - `master` moved the OpenUSD stack to `0.4.0-alpha`; this branch pins `OpenUsd.Rendering` in the test project so the renderer-pick tests can reach the picking backend. Left at `0.1.0-alpha` that pin is below what the viewer now resolves transitively, which is `CS1705`. The pin follows `master`. CI was green on `baaf35e9` with **190 checks passing and none failing**, across every build leg (net472 / net48 / netstandard2.0 / netstandard2.1 / net8.0 / net9.0 / net10.0, Debug and Release) and the full test matrix on Windows and Linux. Since then `master` has been merged twice more and two CI-stability fixes landed (see below), so the current head is re-running. Locally, `dotnet build UA.slnx -t:Rebuild` gives **0 errors** and the 2 pre-existing `CA1861` warnings; Robotics **467** net10.0 / **447** net48, Robot Intent **20** on both, OpenUSD **904**, AOT **121/121**, source generation **3771**. ### Review round Five reviewers over disjoint areas found defects worth calling out, all fixed in `3ffec2c1`: - **A refused mission step was reported `Succeeded`.** The advance path returned the same `false` for "steps exhausted" and "step refused", so a protective stop between steps told a supervisory system the mission had completed while the robot stood halted with work unexecuted. - **Three attacker-controlled NodeIds reached the executor unvalidated** — `Pick.Tool`, `Place.Tool`, `ToolChange.DockStation`. `DockStation` drives a physical tool-exchange motion to a location the server never authorised. The replacement test enumerates NodeId members by reflection so the next one added cannot be missed. - **A lock-order inversion that loses a stop.** `CanCancel` and the mission condition evaluator ran under the host lock while the executor's own thread re-entered it via `ReportBlendBegin`. - **A non-terminal executor outcome killed the pump** from outside the `try/catch` that exists because the executor is untrusted — `Ready` stayed true and submissions kept being accepted while nothing moved. - **`Pause` published `Suspended` while the robot kept moving.** Now queue-only and honest about it; the specification gained the matching rule ([opcua-drafts `357a558`](marcschier/opcua-drafts#47)). - **`RI-RealTimeChannel` and `RI-Safety` were false claims** — granted on an empty folder with no methods, and on the mere presence of a Mandatory node. - **The Part 10 promotions never promoted.** Namespace-1 BrowseNames declared second members beside the inherited ones; the conformance test could not see it because its lookup stripped the prefix. Fixed upstream and regenerated. - **Renderer picking never worked.** The reflective probe bound a renderer that is not a picking backend and throws off its owner thread. It now binds only `IRenderPickingBackend`, falls back immediately and loudly, and the docs say plainly that renderer picking is unavailable with this package version. - **The client lost command authority the instant it gained it** — monitored items report the current value first, and the lease treated every notification as a loss. ### CI stability fixes for master-side failures The CI split in #4158 turned on legs that had never run, and two of the failures they exposed were real bugs on `master` rather than anything this branch introduced. Both are fixed here because they were blocking every branch: - **ECC key generation was broken on macOS.** `ExportExplicitParameters` throws `PlatformNotSupportedException` there — Apple's SecurityTransforms supports named curves only — and it was called for exactly one value, the curve order `n`. The four curves OPC 10000-12 admits have published orders, so those are carried directly, with `CurveOrderTableMatchesThePlatform` checking each against the platform's own exported order wherever export works, so the constants cannot silently drift. This accounts for the two ECC failures in `test-macOS-latest-Server`. - **The certificate-leak detector raced asynchronous disposal.** It polled a fixed budget and reported whatever it saw, which cannot distinguish a genuine leak (count steady) from a suite still draining fire-and-forget disposals (count falling) — on a loaded agent that produced `Certificate leak detected: 2 instance(s)` with every test passing. It now waits for the count to **settle**, so a real leak is reported sooner than before while a slow drain gets as long as it keeps progressing. Shared by every suite that asserts no certificate leaks. The remaining macOS failures on `master` (`Gds`, `Tools`, `Redundancy.Samples`, `aot-macos`) have different root causes, are not reproducible without a macOS host, and are **not** addressed here. ### Merged with `master` again `master` through `416d619c4` (#4158, the CI matrix split and graduated coverage bands) is merged in as `08ae4528`. One conflict, in `docs/DeveloperGuide.md`, where both sides had deliberately rewritten the coverage-gate section: - `master`'s rewrite is kept in full — graduated `patch.bands`, Codecov being informational-only, and where the numbers surface per CI system. - This branch's paragraph about the `ignore` globs is kept too, re-sited ahead of those subsections. I re-checked it against the merged `coverage-thresholds.json` rather than assuming it still held: the list is still `tests/**`, `samples/**`, `obj`, `bin`, `*.g.cs`, with **`tools/**` absent** — so the warning that anything under `tools/` is measured like product code, and that an assembly no test project references contributes changed lines counted as *uncovered*, is still accurate and worth keeping. - `master`'s "reproduce a coverage failure locally" line supersedes this branch's near-identical one, so only `master`'s survives. Post-merge: `dotnet build UA.slnx -t:Rebuild` **0 errors**, 2 pre-existing `CA1861`; Robotics **467** net10.0 / **447** net48; Robot Intent **20** on both. ### Getting CI green The pipeline had never actually run on this branch until now (it was waiting on the fork-PR approval gate), so the first run surfaced four real problems, each fixed at source: - **Seven build legs failed** because `IntentEnabledRobot` pins `net10.0` for the OpenUSD payload but never opted into `RestrictForLegacyTfm`. On the legacy legs the libraries build as net472/net48, where `Opc.Ua.Types` exposes the `System.Threading.Lock` polyfill publicly, and that collides with the sample's own net10.0 BCL (`CS0433`). `Opc.Ua.OpenUsd.Connector.Viewer` was already using the opt-out for the same reason. - **A Linux-only test failure** turned out to be a real defect, not a platform quirk: `SetPose` published the *commanded* interpolated pose beside joint frames derived from the *solved* joints, so the two disagreed by the inverse-kinematics residual — about 8 mm, constant after any real motion. That is the lying-digital-twin defect one level deeper than the per-step IK fix reached. `SetPose` now derives both from the joint angles. - **Three source-generator tests** asserted the old emitted shape. The clause 6.2 Null-Variant fix was implemented in `ObjectTypeProxyGenerator` so every generated proxy benefits, which changed that shape; the tests are updated and strengthened to pin the `IsNull` branch as well. - **A CI-only timeout** in the simulated arm drain: the executor runs on a manual clock so the tests do not depend on machine speed, but the drain guarded itself with a wall-clock deadline, which put that dependence back. CI runs with coverage instrumentation, which is enough to take a per-step IK probe from ~0.6 s to over 5 s. The guard is now a simulated step budget. One failure was **not** from this PR and did not recur: a Roslyn `AccessViolationException` compiling `Opc.Ua.ISA95` on one leg, while the Debug leg of the same target passed on identical code and the Release build succeeds locally. ## Related Issues - Relates to #3827 — the request that prompted the robotics intent work. - Builds on #4127. The companion specification is drafted in the open at [marcschier/opcua-drafts#47](marcschier/opcua-drafts#47) (`metaverse-specs/robot-intent/`), with the prior art and the reasoning behind each decision in the research document beside it. **Nothing in it is normative or endorsed by the OPC Foundation, and its NodeIds and namespace URI are provisional** — which is the main reason this is a draft. ## Checklist - [x] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [x] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. On the test checkbox, precisely: the whole `UA.slnx` suite now runs on net10.0 with **50 of 52 assemblies fully green**, and `Opc.Ua.Robotics.Tests` passes on **all five** target frameworks including net472, net8.0 and net9.0 which had never previously been exercised. The two remaining failures are reproduced identically at the merge-base and are detailed above, so I have ticked the box; if you would rather it stayed unticked until those two are green, say so and I will revert it. ## Points I would most like feedback on 1. **The namespace and NodeIds are provisional.** They come from a draft specification, not from the OPC Foundation. If this lands before the model is registered, the identifiers will move. 2. **The model lives in `Opc.Ua.Robotics`.** It is standalone on the base UA namespace and takes no dependency on OPC 40010, so a separate `Opc.Ua.RobotIntent` package would arguably be cleaner. Keeping it here was a deliberate call — this is where the robot-facing API already is — but it is the easiest thing to change now and the hardest later. 3. **Removing the convention API from #4127 is a breaking change** for anyone who adopted it. It was explicitly marked non-normative, so I have assumed that is acceptable — please say if a deprecation period is wanted instead. 4. **Serial execution** is a deliberate simplification. Concurrent `None`/`Soft` intents would be an optimisation, not a correction, but it is worth agreeing that reading. 5. **`samples/Robotics/` reorganisation** moves `MinimalRobotServer`. Happy to split that into its own commit if it makes review easier. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa Copilot-Session: a248a589-9a20-4372-868e-5d347e57001b
What this PR changes or proposes
Adds OPC UA — Robot Intent (
metaverse-specs/robot-intent/, Release 0.1.0 draft): an information model for commanding a robot at the level of task intent — move there, follow this path, weld this seam, pick from here, place at that — with a lifecycle that survives the minutes such work actually takes.OPC-UA-Robot-Intent.md— the specification (13 clauses, 4 annexes)OPC-UA-Robot-Intent-Research.md— the prior art, the gaps, and the decisions those gaps forcedOpc.Ua.RobotIntent.NodeSet2.xml/.NodeIds.csv— generated (256 nodes, 69 types)extras/robot-intent/tools/— generator, validator, generated Annex AIt also fixes a defect in both generators: a
DataTypeEncodingbrowses asDefault Binaryin namespace 0, and Vision and Robot Intent were both emitting1:Default Binary. Vision is regenerated — 9 lines, no NodeId churn.Why
OPC 40010-1 Robotics describes robot topology in detail and defines no motion verbs at all. Its entire actuation surface is
Start,Stopand loading a named program. A conformant client can discover everything about a robot's construction and cannot ask it to move anywhere. Every integration above "run program 7" is therefore bespoke, even though every vendor already has joint, linear and circular moves, a speed, an acceleration, a blend, a tool frame and a work frame.This supplies the verbs, and nothing else, so the two compose rather than compete.
The lifecycle is the harder half. A motion takes seconds and a pick takes a minute, while OPC 10000-4 §5.12.2 discards a method result when the Session ends "independent of the task actually performed at the Server" — so a synchronous motion method loses the outcome of work that has already physically happened. Submission therefore returns a Part 10 program instance the client watches, which is the resolution OPC 10000-10 §4.1 already reaches for exactly this case.
Design decisions worth review
SupportedIntentsrather than a probe for BrowseNames.A/B/Cfields ofThreeDOrientationcarry no convention of their own. Annex C carries the normative bidirectionalThreeDFrameconversion, including the clamp that stops a pole orientation becoming a domain error.ExecutionStaterefines the Part 10 state rather than restating it.Queued,Cancellingand the three terminal outcomes cannot be told apart fromCurrentStatealone; §6.3 tabulates every legal pairing, so the refinement is checkable rather than asserted.Pick/PlacereferenceLocationnodes, not station strings, andFastenreferences a joint in OPC 40450/40451 rather than restating tightening. A free-text identifier would be a second definition of a fact the address space already holds.MC_BufferModeand concurrency is VDA 5050blockingType, both adopted unchanged. No OPC UA specification defines either, and both are already implemented across the industry.ContentFilterso nobody has to write a parser for an invented expression language, and an empty transition array leaves a mission the flat sequence it was.HasIntentController(Annex B).Real-time: a division of labour, not an exclusion
Trajectory execution is in scope — the whole time-parameterised path is handed over once and the robot's own motion kernel runs it, which is what ROS
FollowJointTrajectoryand the PLCopen buffered path blocks already do. Streaming control is not expressible over OPC UA client/server, so the model brokers a channel instead: it describes and leases RTDE, EGM, FRI, RSI, MotoROS2 or OPC UA FX (OPC 10000-80…84), and the samples never traverse OPC UA. Same shape as the Vision model brokering a media endpoint rather than carrying pixels.Safety: awareness and a refusal duty, explicitly not a rating
This one cannot be brought into scope as asked, and the specification says so rather than working around it.
OPC 10000-15 carries cyclic safety data from a SafetyProvider to a SafetyConsumer; the consumer's request holds an identifier, a monitoring number and one octet of explicitly non-safety flags, so a caller has no channel through which to supply safety-rated arguments. Every safety fieldbus — PROFIsafe, CIP Safety, FSoE, openSAFETY — expresses a safety command as a continuously asserted cyclic signal, because the integrity argument rests on the fail-safe state that follows when assertion stops. A Method call has no defined behaviour when it stops being called, and therefore cannot be a safety function however it is labelled.
So the model reports what the safety system is enforcing (
SafetyStateType, with the IEC 61800-5-2 function vocabulary) and refuses work that would exceed it (§10.4), and states plainly that neither of those makes anything safe. A Server may observe the safety system and refuse on what it sees; it may never instruct it.Standards check
The widely repeated claim that OPC 40010 Parts 2 (Skills) and 3 (Motion Program) are in development was tested against primary sources and is not supported — the OPC Foundation reference index lists only Part 1. What exists is
opcua-skills/skill-nodeset, a VDMA SOArc prototype stalled at v0.0.5 since 2020 underhttp://opcfoundation.org/UA/Skills/. That namespace is avoided and the prior art is cited (Annex D). Whether a non-public VDMA working draft exists is the one question public research cannot close.Type
Checklist
metaverse-specs/validate_all.py --self-containedandextras/robot-intent/tools/validate_local.py)Validation
metaverse-specs/validate_all.py --self-contained— all extensions OKmain— 83 added, 0 removed, 0 renumbered for Robot Intent; unchanged for VisionThe validator re-derives everything from the committed artifacts rather than asking the generator what it emitted, and cross-checks the specification against the model in both directions: every type and enumeration literal the model declares must be named in the prose, and every
ns=1;i=<n>the prose cites must exist in the model. That check earned its keep twice — 48 unnamed members the first time, and another 48 when the scope was enlarged.It is mutation-tested against 17 defects and catches all of them, restoring the tree byte-exactly afterwards. That includes the case that is easy to miss — a defect introduced in the generator and then fully regenerated, so the NodeSet, the CSV and Annex A all agree with each other and only the specification invariants can catch it — plus, since the scope grew, a process intent reparented off its abstract base, a transition condition swapped off
ContentFilter, and a mandatory safety member demoted to optional.Note for the reviewer
metaverse-specs/README.mdwas rebased over the OPC Foundation review move (#44/#46). Therelease-spec-linkmarkers and the review notice are preserved verbatim; the section heading is now "The OpenUSD pair: two parts, two directions" because the tree holds four specifications rather than two, andvision/androbot-intent/are listed alongside them.Related
The .NET stack implementation is now open as a draft at OPCFoundation/UA-.NETStandard#4165. It generates the model from this NodeSet, so the two move together — 149 tests pass there on net48 and net10.0.
The encoding BrowseName fix in this PR is what unblocked it: the .NET model source generator fails on
1:Default Binarywith a bareNullReferenceExceptionthat names no node.